---
id: http
title: 19. 远程请求
sidebar_label: 19. 远程请求 (HttpClient)
---

## 19.1 关于远程请求

在互联网大数据的驱动下，平台或系统免不了需要和第三方进行数据交互，而第三方往往提供了 `RESTful API` 结果规范，这个时候就需要通过 `Http` 请求第三方接口进行数据传输交互。

也就是本章节所说的远程请求。

## 19.2 远程请求的作用

- 跨系统、跨设备通信
- 实现多个系统数据传输交互
- 跨编程语言协同开发

## 19.3 基础使用

### 19.3.1 注册服务

使用之前需在 `Startup.cs` 注册 `远程请求服务`

```cs {3}
public void ConfigureServices(IServiceCollection services)
{
    services.AddRemoteRequest();
}
```

### 19.3.2 使用方式

- `IHttpDispatchProxy` 代理方式

定义代理请求的 `接口` 并继承 `IHttpDispatchProxy` 接口

```cs {2}
[Host("http://47.100.247.61/", 5000)]
public interface IHttp : IHttpDispatchProxy
{
    [Get("api/sysdata/categories")]
    Task<RESTfulResult<List<Data>>> GetCategoryAsync();
}
```

通过构造函数注入 `接口`

```cs {9,16}
using Furion.DynamicApiController;
using Furion.RemoteRequest;

namespace Furion.Application
{
    public class RemoteRequestService : IDynamicApiController
    {
        private readonly IHttp _http;
        public RemoteRequestService(IHttp http)
        {
            _http = http;
        }

        public async Task GetData()
        {
            var data = await _http.GetCategoryAsync();
        }
    }
}
```

- 字符串方式

```cs
var data = await "http://47.100.247.61/api/sysdata/categories".GetAsAsync<object>();
```

## 19.4 使用示例

### 19.4.1 字符串方式

- `[METHOD]Async/[METHOD]AsAsync` 方式

```cs
// ============ GET 请求 ============

// 返回 HttpResponseMessage 对象
var response = await "https://www.furion.pro/data".GetAsync();

// 返回特定类型对象
var data = await "http://47.100.247.61/api/sysdata/categories".GetAsAsync<object>();

// 设置请求报文头
var data = await "http://47.100.247.61/api/sysdata/categories".GetAsAsync<object>(headers: new Dictionary<string, string>{
    {"ipaddress", "127.0.0.1"}
});

// 设置请求拦截
var data = await "http://47.100.247.61/api/sysdata/categories".GetAsAsync<object>(requestInterceptor: request=>{
    request.Headers.Add("Authorization","Bearer token字符串");
});

// 设置HttpClient拦截
var data = await "http://47.100.247.61/api/sysdata/categories".GetAsAsync<object>(httpClientInterceptor: httpClient=>{
});

// ============ HEAD 请求 ============

// 同上

// ============ POST 请求 ============

// 返回 HttpResponseMessage 对象
var response = await "https://www.furion.pro/data".PostAsync(new { parm1="", parm2="" });

// 返回特定类型对象
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(new { parm1="", parm2="" });

// 设置请求报文头
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(new { parm1="", parm2="" }, headers: new Dictionary<string, string>{
    {"ipaddress", "127.0.0.1"}
});

// 设置请求拦截
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(new { parm1="", parm2="" }, requestInterceptor: request=>{
    request.Headers.Add("Authorization","Bearer token字符串");
});

// 设置HttpClient拦截
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(httpClientInterceptor: httpClient=>{
});

// ============ PUT 请求 ============

// 同上

// ============ PATCH 请求 ============

// 同上

// ============ DELETE 请求 ============

// 同上
```

- `SendAsync/SendAsAsync` 方式

```cs
// 返回 HttpResponseMessage 对象
var response = await "https://www.furion.pro/data".SendAsync(HttpMethod.Get);

// 返回特定类型对象
var data = await "http://47.100.247.61/api/sysdata/categories".SendAsAsync<object>(HttpMethod.Get);

// 设置请求报文头
var data = await "http://47.100.247.61/api/sysdata/categories".SendAsAsync<object>(HttpMethod.Get, headers: new Dictionary<string, string>{
    {"ipaddress", "127.0.0.1"}
});

// 设置请求拦截
var data = await "http://47.100.247.61/api/sysdata/categories".SendAsAsync<object>(HttpMethod.Post, requestInterceptor: request=>{
    request.Headers.Add("Authorization","Bearer token字符串");
});

// 设置HttpClient拦截
var data = await "http://47.100.247.61/api/sysdata/categories".SendAsAsync<object>(httpClientInterceptor: httpClient=>{
});
```

### 19.4.2 代理方式

- 通过 `特性` 方式配置

```cs {1-3,6,9,12,16,22}
[Host("http://47.100.247.61/", 5000)]   // 配置主机端口
[Header("Authorization", "Bearer 你的token")]   // 配置请求头
[Header("ipaddress", "127.0.0.1")]  // 配置多个请求头
public interface IHttp : IHttpDispatchProxy
{
    [Get("api/sysdata/categories")]
    Task<RESTfulResult<List<Data>>> GetCategory();

    [Get("api/sysdata/{categoryid}/data")]
    Task<RESTfulResult<List<Data2>>> GetData2(int categoryid);

    [Post("api/user/modify")]
    Task<RESTfulResult<object>> PostVoid(object value);

    // 请求拦截器
    static HttpRequestMessage RequestInterceptor(HttpRequestMessage httpRequest, MethodInfo method, object[] args)
    {
        return httpRequest;
    }

    // 响应拦截器
    static HttpResponseMessage ResponseInterceptor(HttpResponseMessage httpResponse, MethodInfo method, object[] args)
    {
        return httpResponse;
    }
}
```

## 19.5 代理高级应用

### 19.5.1 参数处理

- `普通` 参数

```cs
[Get("https://www.furion.pro/getdata/{id}?name={name}")]
Task<Object> GetData(int id, string name);
```

- `数组` 参数解构

```cs
[Get("https://www.furion.pro/getdata/?{...ids}")]
void DeleteIds(int[] ids);
```

- `对象` 参数解构

```cs
[Get("https://www.furion.pro/getdata/?{...user}")]
void DeleteIds([Query]UserDto user);
```

- `对象` 参数

```cs
[Post("https://www.furion.pro/update-user")]
void EditUser(UserDto user);
```

对象类型参数默认以 `Body` 方式传递。

:::important 类对象解构说明

默认情况下，执行 `Get/Head` 请求时不会处理 `类对象` 类型参数，但是如果贴了 `[Query]` 参数后，会自动将对象解析成 `url` 地址格式，如 `?name=百小僧&age=27`。

:::

### 19.5.2 参数验证

代理拦截的方式也支持参数 `验证特性` 使用。如：

```cs
[Get("https://www.furion.pro/getdata/{id}?name={name}")]
Task<Object> GetData([Required, Range(1, 1000)] int id, [MaxLength(32)] string name);
```

另外也支持 `类对象` 方式，如：

```cs
public class User
{
    [Required]  // 必填验证
    [MinLength(4)]  // 最小长度验证
    public string Account { get; set; }

    [Required]    // 必填验证
    [MaxLength(32)]    // 最大长度验证
    public string Password { get; set; }
}
```

```cs
[Post("https://www.furion.pro/getdata/{id}?name={name}")]
Task<Object> GetData(User user);
```

### 19.5.3 返回值处理

每一个请求谓词特性都提供了 `ResponseType` 属性，该属性用来配置返回值类型，默认是 `Object` 类型。`ResponseType` 属性可选配置如下：

- `ResponseType` 响应类型：
  - `Object`：对象类型，默认值，也就是自动序列化成代理的方法返回值类型
  - `Text`：字符串类型
  - `Stream`：流类型，**返回该类型时，需在处理完之后释放流**
  - `ByteArray`：字节数组类型

如：

```cs
// 返回对象 T 类型
[Get("https://www.furion.pro/getdata", ResponseType = ResponseType.Object)]
Task<User> GetData();

// 返回字符串类型，也可以使用 ResponseType.Object
[Get("https://www.furion.pro/getdata", ResponseType = ResponseType.Text)]
Task<string> GetData();

// 返回流类型
[Get("https://www.furion.pro/getdata", ResponseType = ResponseType.Stream)]
Task<Stream> GetData();

// 返回字节数组类型
[Get("https://www.furion.pro/getdata", ResponseType = ResponseType.ByteArray)]
Task<byte[]> GetData();
```

### 19.5.4 请求/响应拦截

`Furion` 框架重复利用了 `C# 8.0+` 的特性，实现了接口中可定义静态方法和实现的机制，如：

```cs {7,15}
public interface IHttp : IHttpDispatchProxy
{
    [Get("https://www.furion.pro/getdata", ResponseType = ResponseType.Object)]
    Task<User> GetData();

    // 请求拦截器
    static HttpRequestMessage RequestInterceptor(HttpRequestMessage httpRequest, MethodInfo method, object[] args)
    {
        // 比如这里写日志

        return httpRequest;
    }

    // 响应拦截器
    static HttpResponseMessage ResponseInterceptor(HttpResponseMessage httpResponse, MethodInfo method, object[] args)
    {
        // 比如这里写日志

        return httpResponse;
    }

    // HttpClient 拦截器
    static HttpClient HttpClientInterceptor(HttpClient httpClient, MethodInfo method, object[] args)
    {
        // 比如这里设置代理

        return httpClient;
    }
}
```

:::important 字符串方式拦截

字符串方式只提供了请求拦截，不提供响应拦截

```cs
// 设置请求拦截
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(new { parm1="", parm2="" }, requestInterceptor: request=>{
    request.Headers.Add("Authorization","Bearer token字符串");
});

// 设置HttpClient拦截
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(httpClientInterceptor: httpClient=>{
});
```

:::

### 19.5.5 `Body` 参数序列化问题

由于一些第三方接口不规范或对参数大小写敏感，这个时候我们可以配置特性的 `PropertyNamingPolicy` 属性，如：

```cs
[Post("https://www.furion.pro/getdata", PropertyNamingPolicy = JsonNamingPolicyOptions.Null)]
Task<User> GetData();
```

- `JsonNamingPolicyOptions` 可选值：
  - `CamelCase`：默认，首字母小写属性名
  - `Null`：保持原有属性名称定义规则

### 19.5.6 `Body` 内容配置

默认情况下，支持 `Body` 参数配置的请求都会序列化成 `Json` 内容配置，我们也可以通过 `HttpContentType` 属性指定，如：

```cs
[Post("https://www.furion.pro/getdata", HttpContentType  = HttpContentTypeOptions.JsonStringContent)]
Task<User> GetData(User user);
```

- `HttpContentTypeOptions` 可选值：
  - `StringContent`：字符串内容
  - `JsonStringContent`：`Json` 字符串内容
  - `XmlStringContent`：`Xml` 字符串内容
  - `MultipartFormDataContent`：`multipart/form-data` 类型内容
  - `FormUrlEncodedContent`：`x-www-form-urlencoded` 类型内容

### 19.5.7 多个请求客户端配置

`Furion` 框架也提供了多个请求客户端配置，可以为多个客户端请求配置默认请求信息，如：

```cs {4,12}
services.AddRemoteRequest(options=>
{
    // 配置 Github 基本信息
    options.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("https://api.github.com/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
    });

    // 配置 Facebook 基本信息
    options.AddHttpClient("facebook", c =>
    {
        c.BaseAddress = new Uri("https://api.facebook.com/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.facebook.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
    });
})
```

**配置了命名客户端后，每次请求都会自动加上这些配置。**

在 `代理请求` 使用

```cs
[Get("api/getdata"), Client("github")]
Task<User> GetData();

[Put("api/getdata"), Client("facebook")]
Task<User> GetData();
```

在 `字符串拓展` 使用

```cs
// 设置请求拦截
var data = await "http://47.100.247.61/api/sysdata/categories".PostAsAsync<object>(new { parm1="", parm2="" }, clientName = "github");
```

## 19.6 代理内置特性

### 19.6.1 接口特性

- `主机` 特性
  - `[Host]`：配置主机地址和端口
- `请求头` 特性
  - `[Header]`：配置请求报文头，支持多个
- `客户端` 特性
  - `[Client]`：配置客户端

接口的特性会影响所有的成员方法，也就是会应用到每一个方法中，当然方法可可以重写或忽略。

### 19.6.2 方法特性

- `请求谓词` 特性
  - `[Get]`：`Get` 请求方式
  - `[Post]`：`Post` 请求方式
  - `[Put]`：`Put` 请求方式
  - `[Delete]`：`Delete` 请求方式
  - `[Patch]`：`Patch` 请求方式
  - `[Head]`：`Head` 请求方式
- `主机` 特性
  - `[Host]`：配置主机地址和端口
- `请求头` 特性
  - `[Header]`：配置请求报文头，支持多个
- `客户端` 特性
  - `[Client]`：配置客户端
- `内容类型` 特性
  - `[MediaTypeHeader]`：配置内容类型

### 19.6.3 方法参数特性

- `[Query]`：自动将参数替换地址中的占位符，占位符格式 `{参数名}`，如：`https://www.furion.pro/user/{id}`，默认基元类型或基元类型数组应用该特性
- `[Body]`：自动将参数添加到请求报文体中，默认非基元类型会引用该特性。

## 19.7 关于同步请求

`Furion` 框架内部默认不提供同步请求操作，建议总是使用异步的方式请求。如在不能使用异步的情况下，可自行转换为同步执行。

## 19.8 异常处理

默认情况下，如果接口请求异常会抛出请求异常，有时这不是我们想要的结果，我们希望如果接口请求异常，那么直接返回默认值即可，这时候我们只需要在接口或方法贴 `[Safety]` 特性即可。

## 19.9 反馈与建议

:::note 与我们交流

给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。

:::

---

:::note 了解更多

想了解更多 `日志` 知识可查阅 [ASP.NET Core - HTTP 请求](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0) 章节

:::
